/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.applink.sell_proof.util;

import java.io.Closeable;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

/**
 * Reads a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
 * encoded value as a stream of tokens. This stream includes both literal values
 * (strings, numbers, booleans, and nulls) as well as the begin and end
 * delimiters of objects and arrays. The tokens are traversed in depth-first
 * order, the same order that they appear in the JSON document. Within JSON
 * objects, name/value pairs are represented by a single token.
 * 
 * <h3>Parsing JSON</h3> To create a recursive descent parser for your own JSON
 * streams, first create an entry point method that creates a {@code JsonReader}
 * .
 * 
 * <p>
 * Next, create handler methods for each structure in your JSON text. You'll
 * need a method for each object type and for each array type.
 * <ul>
 * <li>Within <strong>array handling</strong> methods, first call
 * {@link #beginArray} to consume the array's opening bracket. Then create a
 * while loop that accumulates values, terminating when {@link #hasNext} is
 * false. Finally, read the array's closing bracket by calling {@link #endArray}.
 * <li>Within <strong>object handling</strong> methods, first call
 * {@link #beginObject} to consume the object's opening brace. Then create a
 * while loop that assigns values to local variables based on their name. This
 * loop should terminate when {@link #hasNext} is false. Finally, read the
 * object's closing brace by calling {@link #endObject}.
 * </ul>
 * <p>
 * When a nested object or array is encountered, delegate to the corresponding
 * handler method.
 * 
 * <p>
 * When an unknown name is encountered, strict parsers should fail with an
 * exception. Lenient parsers should call {@link #skipValue()} to recursively
 * skip the value's nested tokens, which may otherwise conflict.
 * 
 * <p>
 * If a value may be null, you should first check using {@link #peek()}. Null
 * literals can be consumed using either {@link #nextNull()} or
 * {@link #skipValue()}.
 * 
 * <h3>Example</h3>
 * Suppose we'd like to parse a stream of messages such as the following:
 * 
 * <pre>
 * {@code
 * [
 *   {
 *     "id": 912345678901,
 *     "text": "How do I read JSON on Android?",
 *     "geo": null,
 *     "user": {
 *       "name": "android_newb",
 *       "followers_count": 41
 *      }
 *   },
 *   {
 *     "id": 912345678902,
 *     "text": "@android_newb just use android.util.JsonReader!",
 *     "geo": [50.454722, -104.606667],
 *     "user": {
 *       "name": "jesse",
 *       "followers_count": 2
 *     }
 *   }
 * ]}
 * </pre>
 * 
 * This code implements the parser for the above structure:
 * 
 * <pre>
 * {@code
 * 
 *   public List<Message> readJsonStream(InputStream in) throws IOException {
 *     JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
 *     return readMessagesArray(reader);
 *   }
 * 
 *   public List<Message> readMessagesArray(JsonReader reader) throws IOException {
 *     List<Message> messages = new ArrayList<Message>();
 * 
 *     reader.beginArray();
 *     while (reader.hasNext()) {
 *       messages.add(readMessage(reader));
 *     }
 *     reader.endArray();
 *     return messages;
 *   }
 * 
 *   public Message readMessage(JsonReader reader) throws IOException {
 *     long id = -1;
 *     String text = null;
 *     User user = null;
 *     List<Double> geo = null;
 * 
 *     reader.beginObject();
 *     while (reader.hasNext()) {
 *       String name = reader.nextName();
 *       if (name.equals("id")) {
 *         id = reader.nextLong();
 *       } else if (name.equals("text")) {
 *         text = reader.nextString();
 *       } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
 *         geo = readDoublesArray(reader);
 *       } else if (name.equals("user")) {
 *         user = readUser(reader);
 *       } else {
 *         reader.skipValue();
 *       }
 *     }
 *     reader.endObject();
 *     return new Message(id, text, user, geo);
 *   }
 * 
 *   public List<Double> readDoublesArray(JsonReader reader) throws IOException {
 *     List<Double> doubles = new ArrayList<Double>();
 * 
 *     reader.beginArray();
 *     while (reader.hasNext()) {
 *       doubles.add(reader.nextDouble());
 *     }
 *     reader.endArray();
 *     return doubles;
 *   }
 * 
 *   public User readUser(JsonReader reader) throws IOException {
 *     String username = null;
 *     int followersCount = -1;
 * 
 *     reader.beginObject();
 *     while (reader.hasNext()) {
 *       String name = reader.nextName();
 *       if (name.equals("name")) {
 *         username = reader.nextString();
 *       } else if (name.equals("followers_count")) {
 *         followersCount = reader.nextInt();
 *       } else {
 *         reader.skipValue();
 *       }
 *     }
 *     reader.endObject();
 *     return new User(username, followersCount);
 *   }}
 * </pre>
 * 
 * <h3>Number Handling</h3>
 * This reader permits numeric values to be read as strings and string values to
 * be read as numbers. For example, both elements of the JSON array
 * {@code [1, "1"]} may be read using either {@link #nextInt} or
 * {@link #nextString}. This behavior is intended to prevent lossy numeric
 * conversions: double is JavaScript's only numeric type and very large values
 * like {@code 9007199254740993} cannot be represented exactly on that platform.
 * To minimize precision loss, extremely large values should be written and read
 * as strings in JSON.
 * 
 * <p>
 * Each {@code JsonReader} may be used to read a single JSON stream. Instances
 * of this class are not thread safe.
 */
public final class JsonReader implements Closeable {

	private static final String TRUE = "true";
	private static final String FALSE = "false";

	/** The input JSON. */
	private final Reader in;

	/** True to accept non-spec compliant JSON */
	private boolean lenient = false;

	/**
	 * Use a manual buffer to easily read and unread upcoming characters, and
	 * also so we can create strings without an intermediate StringBuilder. We
	 * decode literals directly out of this buffer, so it must be at least as
	 * long as the longest token that can be reported as a number.
	 */
	private final char[] buffer = new char[1024];
	private int pos = 0;
	private int limit = 0;

	private final List<JsonScope> stack = new ArrayList<JsonScope>();
	{
		push(JsonScope.EMPTY_DOCUMENT);
	}

	/**
	 * The type of the next token to be returned by {@link #peek} and
	 * {@link #advance}. If null, peek() will assign a value.
	 */
	private JsonToken token;

	/** The text of the next name. */
	private String name;

	/*
	 * For the next literal value, we may have the text value, or the position
	 * and length in the buffer.
	 */
	private String value;
	private int valuePos;
	private int valueLength;

	/** True if we're currently handling a skipValue() call. */
	private boolean skipping = false;

	/**
	 * Creates a new instance that reads a JSON-encoded stream from {@code in}.
	 */
	public JsonReader(Reader in) {
		if (in == null) {
			throw new NullPointerException("in == null");
		}
		this.in = in;
	}

	/**
	 * Configure this parser to be be liberal in what it accepts. By default,
	 * this parser is strict and only accepts JSON as specified by <a
	 * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the
	 * parser to lenient causes it to ignore the following syntax errors:
	 * 
	 * <ul>
	 * <li>End of line comments starting with {@code //} or {@code #} and ending
	 * with a newline character.
	 * <li>C-style comments starting with {@code /*} and ending with {@code *}
	 * {@code /}. Such comments may not be nested.
	 * <li>Names that are unquoted or {@code 'single quoted'}.
	 * <li>Strings that are unquoted or {@code 'single quoted'}.
	 * <li>Array elements separated by {@code ;} instead of {@code ,}.
	 * <li>Unnecessary array separators. These are interpreted as if null was
	 * the omitted value.
	 * <li>Names and values separated by {@code =} or {@code =>} instead of
	 * {@code :}.
	 * <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
	 * </ul>
	 */
	public void setLenient(boolean lenient) {
		this.lenient = lenient;
	}

	/**
	 * Consumes the next token from the JSON stream and asserts that it is the
	 * beginning of a new array.
	 */
	public void beginArray() throws IOException {
		expect(JsonToken.BEGIN_ARRAY);
	}

	/**
	 * Consumes the next token from the JSON stream and asserts that it is the
	 * end of the current array.
	 */
	public void endArray() throws IOException {
		expect(JsonToken.END_ARRAY);
	}

	/**
	 * Consumes the next token from the JSON stream and asserts that it is the
	 * beginning of a new object.
	 */
	public void beginObject() throws IOException {
		expect(JsonToken.BEGIN_OBJECT);
	}

	/**
	 * Consumes the next token from the JSON stream and asserts that it is the
	 * end of the current array.
	 */
	public void endObject() throws IOException {
		expect(JsonToken.END_OBJECT);
	}

	/**
	 * Consumes {@code expected}.
	 */
	private void expect(JsonToken expected) throws IOException {
		peek();
		if (token != expected) {
			throw new IllegalStateException("Expected " + expected
					+ " but was " + peek());
		}
		advance();
	}

	/**
	 * Returns true if the current array or object has another element.
	 */
	public boolean hasNext() throws IOException {
		peek();
		return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY;
	}

	/**
	 * Returns the type of the next token without consuming it.
	 */
	public JsonToken peek() throws IOException {
		if (token != null) {
			return token;
		}

		switch (peekStack()) {
		case EMPTY_DOCUMENT:
			replaceTop(JsonScope.NONEMPTY_DOCUMENT);
			JsonToken firstToken = nextValue();
			if (token != JsonToken.BEGIN_ARRAY
					&& token != JsonToken.BEGIN_OBJECT) {
				throw new IOException(
						"Expected JSON document to start with '[' or '{' but was "
								+ token);
			}
			return firstToken;
		case EMPTY_ARRAY:
			return nextInArray(true);
		case NONEMPTY_ARRAY:
			return nextInArray(false);
		case EMPTY_OBJECT:
			return nextInObject(true);
		case DANGLING_NAME:
			return objectValue();
		case NONEMPTY_OBJECT:
			return nextInObject(false);
		case NONEMPTY_DOCUMENT:
			return token = JsonToken.END_DOCUMENT;
		case CLOSED:
			throw new IllegalStateException("JsonReader is closed");
		default:
			throw new AssertionError();
		}
	}

	/**
	 * Advances the cursor in the JSON stream to the next token.
	 */
	private JsonToken advance() throws IOException {
		peek();

		JsonToken result = token;
		token = null;
		value = null;
		name = null;
		return result;
	}

	/**
	 * Returns the next token, a {@link JsonToken#NAME property name}, and
	 * consumes it.
	 * 
	 * @throws IOException
	 *             if the next token in the stream is not a property name.
	 */
	public String nextName() throws IOException {
		peek();
		if (token != JsonToken.NAME) {
			throw new IllegalStateException("Expected a name but was " + peek());
		}
		String result = name;
		advance();
		return result;
	}

	/**
	 * Returns the {@link JsonToken#STRING string} value of the next token,
	 * consuming it. If the next token is a number, this method will return its
	 * string form.
	 * 
	 * @throws IllegalStateException
	 *             if the next token is not a string or if this reader is
	 *             closed.
	 */
	public String nextString() throws IOException {
		peek();
		if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
			throw new IllegalStateException("Expected a string but was "
					+ peek());
		}

		String result = value;
		advance();
		return result;
	}

	/**
	 * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token,
	 * consuming it.
	 * 
	 * @throws IllegalStateException
	 *             if the next token is not a boolean or if this reader is
	 *             closed.
	 */
	public boolean nextBoolean() throws IOException {
		peek();
		if (token != JsonToken.BOOLEAN) {
			throw new IllegalStateException("Expected a boolean but was "
					+ token);
		}

		boolean result = (value == TRUE);
		advance();
		return result;
	}

	/**
	 * Consumes the next token from the JSON stream and asserts that it is a
	 * literal null.
	 * 
	 * @throws IllegalStateException
	 *             if the next token is not null or if this reader is closed.
	 */
	public void nextNull() throws IOException {
		peek();
		if (token != JsonToken.NULL) {
			throw new IllegalStateException("Expected null but was " + token);
		}

		advance();
	}

	/**
	 * Returns the {@link JsonToken#NUMBER double} value of the next token,
	 * consuming it. If the next token is a string, this method will attempt to
	 * parse it as a double using {@link Double#parseDouble(String)}.
	 * 
	 * @throws IllegalStateException
	 *             if the next token is not a literal value.
	 */
	public double nextDouble() throws IOException {
		peek();
		if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
			throw new IllegalStateException("Expected a double but was "
					+ token);
		}
		double result;
		try {
			result = Double.parseDouble(value);
		} catch (NumberFormatException e) {
			result = 0;
		}
		advance();
		return result;
	}

	/**
	 * Returns the {@link JsonToken#NUMBER long} value of the next token,
	 * consuming it. If the next token is a string, this method will attempt to
	 * parse it as a long. If the next token's numeric value cannot be exactly
	 * represented by a Java {@code long}, this method throws.
	 * 
	 * @throws IllegalStateException
	 *             if the next token is not a literal value.
	 * @throws NumberFormatException
	 *             if the next literal value cannot be parsed as a number, or
	 *             exactly represented as a long.
	 */
	public long nextLong() throws IOException {
		peek();
		if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
			throw new IllegalStateException("Expected a long but was " + token);
		}

		long result;
		// return 0 when value is ""
		if (value.trim().length() == 0) {
			advance();
			return 0;
		}

		try {
			result = Long.parseLong(value);
		} catch (NumberFormatException ignored) {
			double asDouble = Double.parseDouble(value); // don't catch this
															// NumberFormatException
			result = (long) asDouble;
			if ((double) result != asDouble) {
				throw new NumberFormatException(value);
			}
		}

		advance();
		return result;
	}

	/**
	 * Returns the {@link JsonToken#NUMBER int} value of the next token,
	 * consuming it. If the next token is a string, this method will attempt to
	 * parse it as an int. If the next token's numeric value cannot be exactly
	 * represented by a Java {@code int}, this method throws.
	 * 
	 * @throws IllegalStateException
	 *             if the next token is not a literal value.
	 * @throws NumberFormatException
	 *             if the next literal value cannot be parsed as a number, or
	 *             exactly represented as an int.
	 */
	public int nextInt() throws IOException {
		peek();
		if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
			throw new IllegalStateException("Expected an int but was " + token);
		}

		int result;
		// return 0 when value is ""
		if (value.trim().length() == 0) {
			advance();
			return 0;
		}
		try {
			result = Integer.parseInt(value);
		} catch (NumberFormatException ignored) {
			double asDouble = Double.parseDouble(value); // don't catch this
															// NumberFormatException
			result = (int) asDouble;
			if ((double) result != asDouble) {
				throw new NumberFormatException(value);
			}
		}

		advance();
		return result;
	}

	/**
	 * Closes this JSON reader and the underlying {@link Reader}.
	 */
	public void close() throws IOException {
		value = null;
		token = null;
		stack.clear();
		stack.add(JsonScope.CLOSED);
		in.close();
	}

	/**
	 * Skips the next value recursively. If it is an object or array, all nested
	 * elements are skipped. This method is intended for use when the JSON token
	 * stream contains unrecognized or unhandled values.
	 */
	public void skipValue() throws IOException {
		skipping = true;
		try {
			int count = 0;
			do {
				JsonToken token = advance();
				if (token == JsonToken.BEGIN_ARRAY
						|| token == JsonToken.BEGIN_OBJECT) {
					count++;
				} else if (token == JsonToken.END_ARRAY
						|| token == JsonToken.END_OBJECT) {
					count--;
				}
			} while (count != 0);
		} finally {
			skipping = false;
		}
	}

	private JsonScope peekStack() {
		return stack.get(stack.size() - 1);
	}

	private JsonScope pop() {
		return stack.remove(stack.size() - 1);
	}

	private void push(JsonScope newTop) {
		stack.add(newTop);
	}

	/**
	 * Replace the value on the top of the stack with the given value.
	 */
	private void replaceTop(JsonScope newTop) {
		stack.set(stack.size() - 1, newTop);
	}

	private JsonToken nextInArray(boolean firstElement) throws IOException {
		if (firstElement) {
			replaceTop(JsonScope.NONEMPTY_ARRAY);
		} else {
			/* Look for a comma before each element after the first element. */
			switch (nextNonWhitespace()) {
			case ']':
				pop();
				return token = JsonToken.END_ARRAY;
			case ';':
				checkLenient(); // fall-through
			case ',':
				break;
			default:
				throw syntaxError("Unterminated array");
			}
		}

		switch (nextNonWhitespace()) {
		case ']':
			if (firstElement) {
				pop();
				return token = JsonToken.END_ARRAY;
			}
			// fall-through to handle ",]"
		case ';':
		case ',':
			/* In lenient mode, a 0-length literal means 'null' */
			checkLenient();
			pos--;
			value = "null";
			return token = JsonToken.NULL;
		default:
			pos--;
			return nextValue();
		}
	}

	private JsonToken nextInObject(boolean firstElement) throws IOException {
		/*
		 * Read delimiters. Either a comma/semicolon separating this and the
		 * previous name-value pair, or a close brace to denote the end of the
		 * object.
		 */
		if (firstElement) {
			/* Peek to see if this is the empty object. */
			switch (nextNonWhitespace()) {
			case '}':
				pop();
				return token = JsonToken.END_OBJECT;
			default:
				pos--;
			}
		} else {
			switch (nextNonWhitespace()) {
			case '}':
				pop();
				return token = JsonToken.END_OBJECT;
			case ';':
			case ',':
				break;
			default:
				throw syntaxError("Unterminated object");
			}
		}

		/* Read the name. */
		int quote = nextNonWhitespace();
		switch (quote) {
		case '\'':
			checkLenient(); // fall-through
		case '"':
			name = nextString((char) quote);
			break;
		default:
			checkLenient();
			pos--;
			name = nextLiteral(false);
			// CMV hack
			// if (name.isEmpty) {
			if (name == null || name.length() == 0) {
				throw syntaxError("Expected name");
			}
		}

		replaceTop(JsonScope.DANGLING_NAME);
		return token = JsonToken.NAME;
	}

	private JsonToken objectValue() throws IOException {
		/*
		 * Read the name/value separator. Usually a colon ':'. In lenient mode
		 * we also accept an equals sign '=', or an arrow "=>".
		 */
		switch (nextNonWhitespace()) {
		case ':':
			break;
		case '=':
			checkLenient();
			if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
				pos++;
			}
			break;
		default:
			throw syntaxError("Expected ':'");
		}

		replaceTop(JsonScope.NONEMPTY_OBJECT);
		return nextValue();
	}

	private JsonToken nextValue() throws IOException {
		int c = nextNonWhitespace();
		switch (c) {
		case '{':
			push(JsonScope.EMPTY_OBJECT);
			return token = JsonToken.BEGIN_OBJECT;

		case '[':
			push(JsonScope.EMPTY_ARRAY);
			return token = JsonToken.BEGIN_ARRAY;

		case '\'':
			checkLenient(); // fall-through
		case '"':
			value = nextString((char) c);
			return token = JsonToken.STRING;

		default:
			pos--;
			return readLiteral();
		}
	}

	/**
	 * Returns true once {@code limit - pos >= minimum}. If the data is
	 * exhausted before that many characters are available, this returns false.
	 */
	private boolean fillBuffer(int minimum) throws IOException {
		if (limit != pos) {
			limit -= pos;
			System.arraycopy(buffer, pos, buffer, 0, limit);
		} else {
			limit = 0;
		}

		pos = 0;
		int total;
		while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) {
			limit += total;
			if (limit >= minimum) {
				return true;
			}
		}
		return false;
	}

	private int nextNonWhitespace() throws IOException {
		while (pos < limit || fillBuffer(1)) {
			int c = buffer[pos++];
			switch (c) {
			case '\t':
			case ' ':
			case '\n':
			case '\r':
				continue;

			case '/':
				if (pos == limit && !fillBuffer(1)) {
					return c;
				}

				checkLenient();
				char peek = buffer[pos];
				switch (peek) {
				case '*':
					// skip a /* c-style comment */
					pos++;
					if (!skipTo("*/")) {
						throw syntaxError("Unterminated comment");
					}
					pos += 2;
					continue;

				case '/':
					// skip a // end-of-line comment
					pos++;
					skipToEndOfLine();
					continue;

				default:
					return c;
				}

			case '#':
				/*
				 * Skip a # hash end-of-line comment. The JSON RFC doesn't
				 * specify this behaviour, but it's required to parse existing
				 * documents. See http://b/2571423.
				 */
				checkLenient();
				skipToEndOfLine();
				continue;

			default:
				return c;
			}
		}

		throw syntaxError("End of input");
	}

	private void checkLenient() throws IOException {
		if (!lenient) {
			throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON");
		}
	}

	/**
	 * Advances the position until after the next newline character. If the line
	 * is terminated by "\r\n", the '\n' must be consumed as whitespace by the
	 * caller.
	 */
	private void skipToEndOfLine() throws IOException {
		while (pos < limit || fillBuffer(1)) {
			char c = buffer[pos++];
			if (c == '\r' || c == '\n') {
				break;
			}
		}
	}

	private boolean skipTo(String toFind) throws IOException {
		outer: for (; pos + toFind.length() < limit
				|| fillBuffer(toFind.length()); pos++) {
			for (int c = 0; c < toFind.length(); c++) {
				if (buffer[pos + c] != toFind.charAt(c)) {
					continue outer;
				}
			}
			return true;
		}
		return false;
	}

	/**
	 * Returns the string up to but not including {@code quote}, unescaping any
	 * character escape sequences encountered along the way. The opening quote
	 * should have already been read. This consumes the closing quote, but does
	 * not include it in the returned string.
	 * 
	 * @param quote
	 *            either ' or ".
	 * @throws NumberFormatException
	 *             if any unicode escape sequences are malformed.
	 */
	private String nextString(char quote) throws IOException {
		StringBuilder builder = null;
		do {
			/* the index of the first character not yet appended to the builder. */
			int start = pos;
			while (pos < limit) {
				int c = buffer[pos++];

				if (c == quote) {
					if (skipping) {
						return "skipped!";
					} else if (builder == null) {
						return new String(buffer, start, pos - start - 1);
					} else {
						builder.append(buffer, start, pos - start - 1);
						return builder.toString();
					}

				} else if (c == '\\') {
					if (builder == null) {
						builder = new StringBuilder();
					}
					builder.append(buffer, start, pos - start - 1);
					builder.append(readEscapeCharacter());
					start = pos;
				}
			}

			if (builder == null) {
				builder = new StringBuilder();
			}
			builder.append(buffer, start, pos - start);
		} while (fillBuffer(1));

		throw syntaxError("Unterminated string");
	}

	/**
	 * Reads the value up to but not including any delimiter characters. This
	 * does not consume the delimiter character.
	 * 
	 * @param assignOffsetsOnly
	 *            true for this method to only set the valuePos and valueLength
	 *            fields and return a null result. This only works if the
	 *            literal is short; a string is returned otherwise.
	 */
	private String nextLiteral(boolean assignOffsetsOnly) throws IOException {
		StringBuilder builder = null;
		valuePos = -1;
		valueLength = 0;
		int i = 0;

		findNonLiteralCharacter: while (true) {
			for (; pos + i < limit; i++) {
				switch (buffer[pos + i]) {
				case '/':
				case '\\':
				case ';':
				case '#':
				case '=':
					checkLenient(); // fall-through
				case '{':
				case '}':
				case '[':
				case ']':
				case ':':
				case ',':
				case ' ':
				case '\t':
				case '\f':
				case '\r':
				case '\n':
					break findNonLiteralCharacter;
				}
			}

			/*
			 * Attempt to load the entire literal into the buffer at once. If we
			 * run out of input, add a non-literal character at the end so that
			 * decoding doesn't need to do bounds checks.
			 */
			if (i < buffer.length) {
				if (fillBuffer(i + 1)) {
					continue;
				} else {
					buffer[limit] = '\0';
					break;
				}
			}

			// use a StringBuilder when the value is too long. It must be an
			// unquoted string.
			if (builder == null) {
				builder = new StringBuilder();
			}
			builder.append(buffer, pos, i);
			valueLength += i;
			pos += i;
			i = 0;
			if (!fillBuffer(1)) {
				break;
			}
		}

		String result;
		if (assignOffsetsOnly && builder == null) {
			valuePos = pos;
			result = null;
		} else if (skipping) {
			result = "skipped!";
		} else if (builder == null) {
			result = new String(buffer, pos, i);
		} else {
			builder.append(buffer, pos, i);
			result = builder.toString();
		}
		valueLength += i;
		pos += i;
		return result;
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + " near " + getSnippet();
	}

	/**
	 * Unescapes the character identified by the character or characters that
	 * immediately follow a backslash. The backslash '\' should have already
	 * been read. This supports both unicode escapes "u000A" and two-character
	 * escapes "\n".
	 * 
	 * @throws NumberFormatException
	 *             if any unicode escape sequences are malformed.
	 */
	private char readEscapeCharacter() throws IOException {
		if (pos == limit && !fillBuffer(1)) {
			throw syntaxError("Unterminated escape sequence");
		}

		char escaped = buffer[pos++];
		switch (escaped) {
		case 'u':
			if (pos + 4 > limit && !fillBuffer(4)) {
				throw syntaxError("Unterminated escape sequence");
			}
			String hex = new String(buffer, pos, 4);
			pos += 4;
			return (char) Integer.parseInt(hex, 16);

		case 't':
			return '\t';

		case 'b':
			return '\b';

		case 'n':
			return '\n';

		case 'r':
			return '\r';

		case 'f':
			return '\f';

		case '\'':
		case '"':
		case '\\':
		default:
			return escaped;
		}
	}

	/**
	 * Reads a null, boolean, numeric or unquoted string literal value.
	 */
	private JsonToken readLiteral() throws IOException {
		value = nextLiteral(true);
		if (valueLength == 0) {
			throw syntaxError("Expected literal value");
		}
		token = decodeLiteral();
		if (token == JsonToken.STRING) {
			checkLenient();
		}
		return token;
	}

	/**
	 * Assigns {@code nextToken} based on the value of {@code nextValue}.
	 */
	private JsonToken decodeLiteral() throws IOException {
		if (valuePos == -1) {
			// it was too long to fit in the buffer so it can only be a string
			return JsonToken.STRING;
		} else if (valueLength == 4
				&& ('n' == buffer[valuePos] || 'N' == buffer[valuePos])
				&& ('u' == buffer[valuePos + 1] || 'U' == buffer[valuePos + 1])
				&& ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2])
				&& ('l' == buffer[valuePos + 3] || 'L' == buffer[valuePos + 3])) {
			value = "null";
			return JsonToken.NULL;
		} else if (valueLength == 4
				&& ('t' == buffer[valuePos] || 'T' == buffer[valuePos])
				&& ('r' == buffer[valuePos + 1] || 'R' == buffer[valuePos + 1])
				&& ('u' == buffer[valuePos + 2] || 'U' == buffer[valuePos + 2])
				&& ('e' == buffer[valuePos + 3] || 'E' == buffer[valuePos + 3])) {
			value = TRUE;
			return JsonToken.BOOLEAN;
		} else if (valueLength == 5
				&& ('f' == buffer[valuePos] || 'F' == buffer[valuePos])
				&& ('a' == buffer[valuePos + 1] || 'A' == buffer[valuePos + 1])
				&& ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2])
				&& ('s' == buffer[valuePos + 3] || 'S' == buffer[valuePos + 3])
				&& ('e' == buffer[valuePos + 4] || 'E' == buffer[valuePos + 4])) {
			value = FALSE;
			return JsonToken.BOOLEAN;
		} else {
			value = new String(buffer, valuePos, valueLength);
			return decodeNumber(buffer, valuePos, valueLength);
		}
	}

	/**
	 * Determine whether the characters is a JSON number. Numbers are of the
	 * form -12.34e+56. Fractional and exponential parts are optional. Leading
	 * zeroes are not allowed in the value or exponential part, but are allowed
	 * in the fraction.
	 * 
	 * <p>
	 * This has a side effect of setting isInteger.
	 */
	private JsonToken decodeNumber(char[] chars, int offset, int length) {
		int i = offset;
		int c = chars[i];

		if (c == '-') {
			c = chars[++i];
		}

		if (c == '0') {
			c = chars[++i];
		} else if (c >= '1' && c <= '9') {
			c = chars[++i];
			while (c >= '0' && c <= '9') {
				c = chars[++i];
			}
		} else {
			return JsonToken.STRING;
		}

		if (c == '.') {
			c = chars[++i];
			while (c >= '0' && c <= '9') {
				c = chars[++i];
			}
		}

		if (c == 'e' || c == 'E') {
			c = chars[++i];
			if (c == '+' || c == '-') {
				c = chars[++i];
			}
			if (c >= '0' && c <= '9') {
				c = chars[++i];
				while (c >= '0' && c <= '9') {
					c = chars[++i];
				}
			} else {
				return JsonToken.STRING;
			}
		}

		if (i == offset + length) {
			return JsonToken.NUMBER;
		} else {
			return JsonToken.STRING;
		}
	}

	/**
	 * Throws a new IO exception with the given message and a context snippet
	 * with this reader's content.
	 */
	public IOException syntaxError(String message) throws IOException {
		throw new JsonSyntaxException(message + " near " + getSnippet());
	}

	private CharSequence getSnippet() {
		StringBuilder snippet = new StringBuilder();
		int beforePos = Math.min(pos, 20);
		snippet.append(buffer, pos - beforePos, beforePos);
		int afterPos = Math.min(limit - pos, 20);
		snippet.append(buffer, pos, afterPos);
		return snippet;
	}

	private static class JsonSyntaxException extends IOException {
		private static final long serialVersionUID = 4651493538877758074L;

		private JsonSyntaxException(String s) {
			super(s);
		}
	}
}
